/**
 * Copyright (c) 2016-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * 
 * @format
 */

'use strict';

const babel = require('babel-core');
const invariant = require('fbjs/lib/invariant');


const t = babel.types;

const React = { name: 'React' };
const ReactNative = { name: 'ReactNative' };
const platform = { name: 'Platform' };
const os = { name: 'OS' };
const select = { name: 'select' };
const requirePattern = { name: 'require' };

const env = { name: 'env' };
const nodeEnv = { name: 'NODE_ENV' };
const processId = { name: 'process' };

const dev = { name: '__DEV__' };

const importMap = new Map([['ReactNative', 'react-native']]);

const isGlobal = binding => !binding;

const isFlowDeclared = binding => t.isDeclareVariable(binding.path);

const isGlobalOrFlowDeclared = binding =>
isGlobal(binding) || isFlowDeclared(binding);

const isToplevelBinding = (binding, isWrappedModule) =>
isGlobal(binding) ||
!binding.scope.parent ||
isWrappedModule && !binding.scope.parent.parent;

const isRequireCall = (node, dependencyId, scope) =>
t.isCallExpression(node) &&
t.isIdentifier(node.callee, requirePattern) &&
checkRequireArgs(node.arguments, dependencyId);

const isImport = (node, scope, patterns) =>
patterns.some(pattern => {
  const importName = importMap.get(pattern.name) || pattern.name;
  return isRequireCall(node, importName, scope);
});

function isImportOrGlobal(node, scope, patterns, isWrappedModule) {
  const identifier = patterns.find(pattern => t.isIdentifier(node, pattern));
  return (
    identifier &&
    isToplevelBinding(scope.getBinding(identifier.name), isWrappedModule) ||
    isImport(node, scope, patterns));

}

const isLeftHandSideOfAssignmentExpression = (node, parent) =>
t.isAssignmentExpression(parent) && parent.left === node;

const isPlatformOS = (node, scope, isWrappedModule) =>
t.isIdentifier(node.property, os) &&
isImportOrGlobal(node.object, scope, [platform], isWrappedModule);

const isReactPlatformOS = (node, scope, isWrappedModule) =>
t.isIdentifier(node.property, os) &&
t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, platform) &&
isImportOrGlobal(
node.object.object,
scope,
[React, ReactNative],
isWrappedModule);


const isProcessEnvNodeEnv = (node, scope) =>
t.isIdentifier(node.property, nodeEnv) &&
t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, env) &&
t.isIdentifier(node.object.object, processId) &&
isGlobal(scope.getBinding(processId.name));

const isPlatformSelect = (node, scope, isWrappedModule) =>
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, platform) &&
t.isIdentifier(node.callee.property, select) &&
isImportOrGlobal(node.callee.object, scope, [platform], isWrappedModule);

const isReactPlatformSelect = (node, scope, isWrappedModule) =>
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.property, select) &&
t.isMemberExpression(node.callee.object) &&
t.isIdentifier(node.callee.object.property, platform) &&
isImportOrGlobal(
node.callee.object.object,
scope,
[React, ReactNative],
isWrappedModule);


const isDev = (node, parent, scope) =>
t.isIdentifier(node, dev) &&
isGlobalOrFlowDeclared(scope.getBinding(dev.name)) &&
!t.isMemberExpression(parent);

function findProperty(objectExpression, key, fallback) {
  const property = objectExpression.properties.find(p => p.key.name === key);
  return property ? property.value : fallback();
}

const inlinePlugin = {
  visitor: {
    Identifier(path, state) {
      if (isDev(path.node, path.parent, path.scope)) {
        path.replaceWith(t.booleanLiteral(state.opts.dev));
      }
    },
    MemberExpression(path, state) {
      const node = path.node;
      const scope = path.scope;
      const opts = state.opts;

      if (!isLeftHandSideOfAssignmentExpression(node, path.parent)) {
        if (
        isPlatformOS(node, scope, opts.isWrapped) ||
        isReactPlatformOS(node, scope, opts.isWrapped))
        {
          path.replaceWith(t.stringLiteral(opts.platform));
        } else if (isProcessEnvNodeEnv(node, scope)) {
          path.replaceWith(
          t.stringLiteral(opts.dev ? 'development' : 'production'));

        }
      }
    },
    CallExpression(path, state) {
      const node = path.node;
      const scope = path.scope;
      const arg = node.arguments[0];
      const opts = state.opts;

      if (
      isPlatformSelect(node, scope, opts.isWrapped) ||
      isReactPlatformSelect(node, scope, opts.isWrapped))
      {
        const fallback = () =>
        findProperty(arg, 'default', () => t.identifier('undefined'));
        const replacement = t.isObjectExpression(arg) ?
        findProperty(arg, opts.platform, fallback) :
        node;

        path.replaceWith(replacement);
      }
    } } };



const plugin = () => inlinePlugin;

function checkRequireArgs(args, dependencyId) {
  const pattern = t.stringLiteral(dependencyId);
  return (
    t.isStringLiteral(args[0], pattern) ||
    t.isMemberExpression(args[0]) &&
    t.isNumericLiteral(args[0].property) &&
    t.isStringLiteral(args[1], pattern));

}







function inline(
filename,
transformResult,
options)
{
  const code = transformResult.code;
  const babelOptions = {
    filename,
    plugins: [[plugin, options]],
    inputSourceMap: transformResult.map,
    sourceMaps: true,
    sourceFileName: filename,
    code: false,
    babelrc: false,
    compact: true };


  const result = transformResult.ast ?
  babel.transformFromAst(transformResult.ast, code, babelOptions) :
  babel.transform(code, babelOptions);const
  ast = result.ast;
  invariant(ast != null, 'Missing AST in babel transform results.');
  return { ast, code: result.code, map: result.map };
}

inline.plugin = inlinePlugin;
module.exports = inline;